 /**
 * @Author: zxc
 * @Date: 2020-08-07 08:29:11
 * @LastEditTime: 2020-08-07 15:53:49
 * @LastEditors: zxc
 * @Description: 基于axios的二次封装
 */
import AxiosInstance, {
  AxiosStatic,
  AxiosPromise,
  AxiosError,
  AxiosRequestConfig,
} from "axios";
import { message } from "antd";
import { setRetryTip } from "../redux/saga/actions/common";
import store from "../redux";
import isRetryAllowed from './isRetryAllowed';
// import LocalStores from "../utils/LocalStore";

//定义请求参数类型声明
type RequestFn = (
  url: string,
  params?: Object,
  data?: Object | null
) => AxiosPromise;

class Http {
  /*
   * 使用 AxiosInstance 而不用 axios.create()这种方式的原因
   * 考虑后期项目需要扩展，需要请求另一个网站的数据
   * axios.create()创建的对象，baseUrl有且只有一个，也就是说只可以指定一个
   * AxiosInstance 可以解决这个问题
   * */
  // 请求对象
  private axios: AxiosStatic = AxiosInstance;
  // 请求失败时的 重试请求的间隔时间
  private retryDelay: number = 1000;
  // 重试次数
  private retry: number = Number(process.env.REACT_APP_RETRY) || 4;

  // 在constructor里进行初始化的设置
  constructor() {
    const { axios } = this;
    axios.defaults.timeout = 10000;
    axios.defaults.baseURL = process.env.REACT_APP_API_URL;
    axios.defaults.headers = {
      "Content-Type": "application/json;charset=UTF-8",
    };
    //执行请求拦截器 和 响应拦截器
    this.useInterceptRequest();
    this.useInterceptResponse();
  }

  //响应拦截器
  // 稍微复杂一点 因为涉及到状态码相关的一套东西,还有 请求出错,如果进行错误判断,
  // 如果后端约定的状态码和当前的不一样,改了就成
  useInterceptResponse() {
    this.axios.interceptors.response.use(
      (res) => {
        // errorCode以及errMsg等源自于后端的约定，有可能是status，有可能是success
        if (res.data.errorCode === "101010500") {
          message.error("服务器错误，请联系管理员");
          return Promise.reject(res.data);
        }

        if (res.data.errorCode === "102022001") {
          message.error("身份信息已过期，请重新登陆");
          // 还需要跳转到 login页面 这个逻辑,但不在这里处理,
        }

        // 然后就是别的情况
        if (res.data.errorCode !== 0) {
          message.error(res.data.errMsg || "服务器异常");
          return Promise.reject(res.data);
        }

        return Promise.resolve(res.data);
      },
      (error: AxiosError) => {
        if (!isRetryAllowed(error)) {
          // 在这里处理的错误，一般都是服务器的问题了
          //多次请求失败情景下的处理
          const { config } = error;
          let retryCont = config.headers["axios-retry"] | 0;
          if (retryCont >= this.retry) {
            // 告诉redux 重试次数已超过指定次数,应该修改状态, 然后组件里自动感应,变为true过后,就会提示用户
            // 提示方式有很多种,就看大家怎么定义 可以用 notification 也可以用ant-design 提供的 Alert组件
            store.dispatch(setRetryTip(true));
            return Promise.reject(error);
          }
          retryCont += 1;
          const backoff = new Promise((resolve) => {
            setTimeout(() => {
              resolve();
            }, this.retryDelay || 1);
          });
          // 修改重试次数
          config.headers["axios-retry"] = retryCont;

          // 必须要在 error中的 config 中去显示绑定才会触发执行
          return backoff.then(() => this.axios(config));
        }
        if (error.response) {
          // http的状态码 非200的时候
          if (error.response.status >= 500) message.error("服务器错误");
        } else if (error.request) {
          // ...
        } else {
          // 其他错误
          message.error(error.message);
        }
        return Promise.reject(error);
      }
    );
  }

  // 请求拦截器
  useInterceptRequest() {
    this.axios.interceptors.request.use(
      async (config: AxiosRequestConfig) => {
        // 这里传进来的config  是我们发出请求的时候，默认的config配置，包括url，method， data...
        // 因为后面需要往服务器上发请求的时候,携带一个token 但是这个token 刚开始是不知道的,需要知道过后,再配置
        const newConfig = config;
        // 获取token
        // 咱们登陆过后,服务器会下发一个token 过来,咱们就存在本地,用localStorage存起来
        // 然后咱们封装一个获取登陆状态的工具函数,在里面去处理 token保存和token过期时刷新token的逻辑
        // 大概是这样子 const token = loginUtils.getToken();
        // 逻辑不放在这里处理.
        const token = await "abs.abs.abs";
        if (token) newConfig.headers.authtoken = token;

        // 处理其它需求

        return newConfig;
      },
      (error: AxiosError) => Promise.reject(error)
    );
  }

  /**
   * @func fetchData 底层公共方法
   * @param type get post
   * @param url 请求的地址
   * @param options 请求的参数
   * @param isComplex 是否平铺 参数 一般用于get但不是绝对
   * @param isComplex---> eg: {a:1,b:2} isComplex为true 时会转换为 a=1&b=2这种格式
   */
  private fetchData(
    type: string,
    url: string,
    options?: Object,
    isComplex?: boolean
  ) {
    if (isComplex) {
      return this.axios[type](url, null, options);
    }
    return this.axios[type](url, options);
  }

  /**
   * @func get 请求封装
   * @param url 请求地址
   * @param params 请求的参数
   */
  public get: RequestFn = (url, params) => {
    //get 可以不传参数
    if (!params) return this.fetchData("get", url);
    // 因为get请求，很有可能会被缓存，所以我们需要给它加一个随机参数，
    // 实现： 因为params 是已经存在的， 我们只需要给它扩展一个随机数的变量即可
    const newParams = Object.assign(params, {
      [`dmx${new Date().getTime()}`]: 1,
    });
    return this.fetchData("get", url, { params: newParams });
  };

  /**
   * @func post 请求封装
   * @param url 请求地址
   * @param params 请求的url上加参数 比如?action=123
   * @param data 请求体 body 内的数据
   */
  public post: RequestFn = (url, params, data) => {
    return this.commonRequest("post", url, params, data);
  };

  /**
   * @func put 请求
   * @param params --- 请求的url上加参数 比如?action=123
   * @param data --- 请求体 body 内的数据
   */
  public put: RequestFn = (url, params, data) => {
    return this.commonRequest("put", url, params, data);
  };

  /**
   * @func patch 请求
   * @param params --- 请求的url上加参数 比如?action=123
   * @param data --- 请求体 body 内的数据
   */
  public patch: RequestFn = (url, params, data) => {
    return this.commonRequest("patch", url, params, data);
  };

  /**
   * @func delete 请求
   * @param params --- 请求的url上加参数 比如?action=123
   * @param data --- 请求体 body 内的数据
   */
  public delete: RequestFn = (url, params, data) => {
    return this.commonRequest("delete", url, params, data);
  };

  /**
   * 因为post put patch delete 逻辑处理其实都一样，所以直接可以把底层函数封装出来们直接调用
   * @param type 请求方式
   * @param url 请求地址
   * @param params 请求的url上加参数 比如?action=123
   * @param data 请求体 body 内的数据
   */
  private commonRequest(
    type: string,
    url: string,
    params?: Object,
    data?: Object | null
  ): AxiosPromise {
    // 合并一下参数
    let options: Object = {
      params,
      data,
    };

    if (params && data === undefined) {
      options = {
        data: params,
      };
    }
    if (data === null) {
      options = {
        params,
      };
    }

    return this.fetchData(type, url, options, true);
  }
}

export default new Http();
